# JS抽象语法树(Abstract syntax tree)
# 初识AST
JS抽象语法树(Abstract syntax tree),简称AST,是JS代码对应的树形结构,它一点也不抽象,修改代码的AST,可以做很多事,ES6转ES5,代码提示插件,代码格式化,写webpack loader等等
# 案例1
来看一段代码
let m = 6
在JS中,这是一段变量声明,在AST中,它叫VariableDeclaration
(变量声明)节点,它还可以被拆分:
1、第一步拆成 let
和 m = 6
let
是变量声明种类,不能再拆m = 6
是变量声明符,在AST中,它叫VariableDeclarator
节点,它还可以继续拆分
2、第二步将m = 6
拆成m
和6
m
是变量声明标识符,不能再拆,在AST中,它属于Identifier
节点6
也不能再拆,在AST中,它属于Literal
节点
这段代码的AST表示为:
在线查看AST的小工具:AST explorer (opens new window)
简化成树状图就是这样
# 案例2
再看一个例子
function welcome(name){
return 'hello ' + name
}
这是一个 FunctionDeclaration
(函数声明)节点,拆分为三块
1、id,就是函数名welcome
{
type: "Identifier",
name: "welcome"
}
2、params,即参数name
{
type: "Identifier",
name: "name"
}
3、body,函数体{ return 'hello ' + name }
,函数体是个 BlockStatement
(语句块),内部是一个ReturnStatement
(return语句),再往下级是一个 BinaryExpression
(二元运算符表达式),再往下就是最基本的 Literal
和Identifier
{
type: "Literal",
value: "hello ",
row: "'hello '"
}
{
type: "Identifier",
name: "name"
}
可以自行将代码输入到AST explorer (opens new window)查看结果
简化成树状图就是这样
除了上面的 FunctionDeclaration
、BlockStatement
、ReturnStatement
等节点,还有什么 ForStatement
(for语句)、ArrayExpression
(数组表达式)、LogicalExpression
(逻辑运算符表达式)……,更多可以参考 babel types (opens new window)
# 实战:做一个webpack loader
了解AST之后,应该知道怎么用它,发挥出它的价值
现在开始做一个webpack loader,作用是将代码里的 == 转换为 ===,操作AST需要用到以下几个包
1、@babel/parser
将代码解析成AST
2、@babel/traverse
对AST增删改操作
3、@babel/generator
将AST生成代码
# 分析
和上面一样,将代码拆分,我们要转换的 == 是在一个 BinaryExpression
(二元运算表达式)里面,将代码输入AST explorer,发现有两个 BinaryExpression
,运算符分别是 == 和 %,== 才是我们要找的那个,因此,只需要遍历全部 BinaryExpression
并判断其 operator
是 ==,然后将 == 替换成 === 即可
# 动手
下面动手新建项目文件夹demo
mkdir demo
初始化
npm init
安装依赖包
npm install @babel/parser @babel/traverse @babel/generator -D
新建三个文件:入口文件index.js、webpack loader文件test-loader.js、webpack配置文件webpack.config.js
index.js
function isEven(n){
if(n%2 == 0){
return true;
}
}
test-loader.js
#!/usr/bin/env node
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default;
module.exports = function (source) {
const ast = parser.parse(source)
traverse(ast, {
enter(path) {
let {operator} = path.node;
// 判断节点类型和操作符是否为 ==
if (path.isBinaryExpression(path.node) && operator === '==') {
path.node.operator = '===';
}
}
})
const output = generator(ast, {}, source);
// 打印出转换后的代码
console.log( output.code );
return output.code
}
webpack.config.js
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].js',
path:path.resolve(__dirname,'')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'test-loader.js') // 在这里引入本地loader
}
]
}
}
执行编译
npm run dev
终端打印出转换后的结果
function isEven(n) {
if (n % 2 === 0) {
return true;
}
}
查看编译生成的main.js,== 被替换成了 ===
eval("function isEven(n) {\n if (n % 2 === 0) {\n return true;\n }\n}\n\n//# sourceURL=webpack:///./index.js?");
loader到这里就算完成了,之后就可以把这个test-loader.js做成一个npm包发布
# Loader进阶
loader插件一般会提供一些配置项,如何获取用户配置呢?需要这样:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};
ES6+转成ES5,可能还需要返回Source Map,那么就需要用this.callback()来返回转换后的内容,它有四个参数,分别是:
this.callback(
// 当无法转换原内容时,给 Webpack 返回一个 Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于把转换后的内容得出原内容的 Source Map,方便调试
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
abstractSyntaxTree?: AST
);